tags:
- Build
在本节,我们将讨论 GCC 是如何一步一步地将 C/C++ 语言源代码编译成二进制机器语言的。也就是 C/C++ 代码是如何变成 ELF 的。
在编译 C/C++ 时,我们常常使用 GCC 作为我们的编译器。GCC 并不是 GNU C Compiler, GCC 是一系列编译器的集合。不仅仅支持 C 语言,还支持 C++ 、 D 、 Objective-C 、 Go 等其他的编程语言。你可以用 gcc -v
来查看你的 GCC 支持哪些语言。
我们把程序从源代码到可执行文件的过程分为两步——编译和链接。在本节,我们将会学习程序是如何编译的。我们把编译的过程共分为 3 步,分别是预处理、编译和汇编。我们先来从机器的视角观察 C/C++ 源代码是怎么样的。
这是一个最简单的例子。这是一段源代码,我将其存放到 hello.c
文件中。我们在文件中输入的所有指令都是 human-friendly 的,而计算机是没有办法理解这个文本文件到底写了什么的。
#include <stdio.h>
int main(){
printf("hello, world\n"); // This prints "hello, world\n"
}
这段源代码以文本文件的形式存储在磁盘上,它在计算机眼中就是这样的:
#include<stdio.h> int main(){printf("hello, world\n");//This prints "hello, world\n"}
而实际上计算机存储的只有 0 和 1 构成的二进制数。所以在计算机眼中,用户实际上输入了一连串的二进制数。如果将这些 ASCII 字符转换成 16 进制的二进制数,就是这样的:
23696e636c7564653c737464696f2e683e20696e74206d61696e28297b7072696e7466282268656c6c6f2c20776f726c645c6e22293b2f2f54686973207072696e7473202268656c6c6f2c20776f726c645c6e22227d
我们有源程序后,我们需要“翻译”这段程序,让计算机能够理解我们想要表达的意思,这个过程就是编译。而编译的第一步,就是预处理。
我们用下面的 shell
指令对程序进行预处理:
gcc -E hello.c -o hello.i
cpp hello.c > hello.i
完成后,编译器就会生成一个 .i
文件,即中间文件(intermediate file)。那么预处理的作用是什么呢?我们把这一步骤叫做预处理,把预处理完成所得到的文件叫中间文件。不难想到,预处理阶段是程序正式进行编译的临门一脚。预处理阶段的作用是处理源文件中以 #
开头的语句。即:
#define
并展开其所定义的宏。#if
、#ifdef
、#endif
等。#include
处,可以递归方式进行处理(复制粘贴)。#pragma
编译指令(编译用)。如果你打开 .i
文件,你会发现一些函数的声明、一些系统信息......各种乱七八糟的东西,这就是因为我们将头文件插入(粘贴)到 #include
的地方了。但总体上,中间文件还是可读的。只不过是将文件进行了一些加工。在中间文件中,你仍然会看到:
int main(){
printf("hello, world\n"); // Why this are not causing error?
}
此外,你还会看到 printf
的函数声明,这也解释了为什么你的程序不会报错。
extern int printf (const char *__restrict __format, ...);
在 C/C++ 中,我们指预处理完成后的 .i
(.ii
in C++)文件为一个个的翻译单元。
我们发现,预处理完成后,我们实际上得到的仍然是高级语言源程序。要将它编程机器可读的二进制程序,编译这一步至关重要。编译过程通过把预处理文件中的高级语言代码进行词法分析、语法分析、语义分析和优化后生成汇编代码文件。狭义上,我们把进行编译处理的程序就叫编译器。
我们用如下的命令可将程序编译为可读的汇编代码文件。
gcc -S hello.i -o hello.s
gcc -S hello.c -o hello.s
/user/lib/gcc/xxxx-linux-gnu/4.1/cc1 hello.c
尽管汇编代码仍是文本,机器无法理解这些代码,但是汇编代码和二进制机器语言代码实际上是一一对应的。你可以把 C/C++ 这种高级语言理解为在汇编语言上的进一步封装/抽象,编译器就是提供这种抽象的核心工具。
一般而言,高级语言都是机器无关的(machine-independent)。但是编译器可以将机器无关的高级语言转换成机器相关的汇编。在 x86 的机器上,编译器会将高级语言源程序编译为 x86 汇编,在 ARM 上,编译器会将高级语言程序编译成 ARM 汇编。
硬件-->(封装抽象)ISA-->(封装抽象)汇编语言-->(封装抽象)高级语言程序
此外,由于高级语言(标准库函数)提供对操作系统的屏蔽和封装,编译器的运行可能还会依赖操作系统。
机器汇编语言被称为低级语言,相比高级语言,汇编语言和机器的二进制指令之间的对应关系非常紧密。也就是说,不同机器架构上使用的汇编语言是不一样的。你可以用下面的指令得到可重定位目标文件。也就是 .o
文件,即目标文件。
gcc –c hello.s –o hello.o
gcc –c hello.c –o hello.o
as hello.s -o hello.o
生成的目标文件中,除了汇编得到的机器码(01数据)之外,还包含元数据。在链接过程中,这些选数据会供链接器使用以完成可执行文件的生成。这些元数据我们在下一节介绍。
至此,广义上的编译就算完成了。